Data FetchingPatterns and Best Practices
ReactやNext.jsでよく使われるData Fetchingのパターンについて学ぶ。
Data Fetching: Data Fetching Patterns and Best Practices | Next.js
hr.icon
### サーバー上でのData Fetching
可能な限り、React Server Components RSCでのデータ取得を推奨。
これにより、以下のメリットがある
バックエンドのデータリソース(例:データベースなど)に直接アクセスできる。
感想:実際、BackendとFrontendが分かれている場合、このメリットは無いかな。
アクセストークン Access TokenやAPIキーなどの機密情報をクライアントに露出させないで、アプリケーションをよりセキュアに保つ
クライアントとサーバー間の往復通信やクライアントのメインスレッドの作業を減らすことで、データの取得とレンダリングを同じ環境で行うことができる。
How React 18 Improves Application Performance – Vercel
感想:
メインスレッドのタスクが減るのは、すごくHappy
Server側の費用が増える可能性ありそうだけど、キャッシュがうまく利用できれば意外と気にならないかも?
クライアント上での複数個別リクエストの代わりに、一回の往復で複数のデータフェッチを行うことができる。
Data Fetching: Data Fetching Patterns and Best Practices | Next.js
感想:
GraphQLとかで出来ていたね。
Server側で何度かリクエストした後、ページ生成したものを送るだけだから、Client側に1度送るだけというのは、そりゃそう。
その後、Server Actionsを使ってデータを変更または更新することができる。
### 必要な場所でのデータ取得
同じデータをツリー内の複数のコンポーネントで使用する必要がある場合、
データをGlobal State frontendに保存したり、コンポーネント間でprops drilling バケツリレー問題する必要は無い。
代わりに、データが必要なコンポーネントでfetch wrapper Next.jsやcache Reactを使うことができる。
fetch wrapper Next.jsリクエストは自動的にメモ化 memorizationされるため、同じデータのために複数のリクエストを行うことによるパフォーマンスへの影響を心配する必要がない
Building Your Application: Caching | Next.js
例
同じデータ(例:現在のユーザー)をツリー内の複数のコンポーネントで使用する必要がある場合
Layout Segment Next.js
親のレイアウトとその子コンポーネント間でデータを渡すことができないから。
nested routing Next.jsにおいてこの概念は重要
Q. なぜ、同じデータを複数の場所で必要とする場合でも、各コンポーネントやレイアウトで必要に応じてデータを取得するのか?。何がHappyなのか?
a. Data Fetchingの複雑性 Complexityが減少し、アプリケーションのmaintainability 保守性が向上。
なぜ実現できるのか?
fetch wrapper Next.jsやcache Reactにメモ化 memorization機能があって、Global State frontendで管理したりprops drilling バケツリレー問題する必要なくなった。
感想
便利なのは分かる。
fetch wrapper Next.jsなど使えない時が心配。
hr.icon
### Streaming React
Streaming ReactとSuspence Reactは、Reactの機能で、UIのレンダリングユニットをクライアントに段階的にレンダリングし、逐次ストリームすることを可能にする
React Server Components RSCとNesting Layouts Next.jsを使うことで、
以下が実現
データを必要としないページの部分を即座にレンダリング
データを取得している部分にはloading states Next.jsを表示することができる。
メリット
ユーザーがページ全体がロードされるのを待たずに、appの操作ができる。(TTI Time to Interactive向上)
https://gyazo.com/ef21656accc3d39d08bc4a1c4120fee2
hr.icon
### parallel 並列とsequential 順次のData Fetching
Reactコンポーネント内でData Fetchingするときは、sequential 順次とparallel 並列の2つのデータ取得パターンを理解しておく必要がある。
https://gyazo.com/c8295f2b825427b62cbddbca2d63d1b7
Sequential Data Fetching
ルート内のリクエストが互いに依存しており、リクエストウォーターフォールを生み出す。
用途
一つのフェッチが他のフェッチの結果に依存している場合
次のフェッチを行う前に特定の条件を満たしたい場合
メリット
リソースを節約できる
主にメモリ
デメリット/注意:
この振る舞いは意図せず発生することもあり、ローディング時間を長くしてしまう原因となることもある
Parallel Data Fetching
ルート内のリクエストが積極的に、そして同時に開始され、データを同時にロード
メリット
クライアントとサーバー間のウォーターフォールが減少し、データをロードするのに要する総時間が短縮
アプリケーションのパフォーマンスが向上し、ユーザーがより速く必要な情報にアクセスできるようになる
デメリット/注意
以下のパターンでに限り、適切に利用できる
すべてのリクエストが互いに独立しているとき
またはデータ取得において順序が重要でないとき
感想
基本は、Parallel Data Fetchingにしたい。
ただ、あるリクエストがあるリクエストのデータに依存している場合があって、その時にSequential Data Fetching使う認識
hr.icon
Sequential Data Fetching
Data Fetching: Data Fetching Patterns and Best Practices | Next.js
Sequential Data Fetchingでは、ネストされたコンポーネントごとにそれぞれデータを取得する場合、そのデータリクエストが異なるものであれば順に取得される。
同じデータに対するリクエストは自動的にメモ化 memorizationされる
fetch wrapper Next.jsやuse reactの機能。
例
Playlistsコンポーネントは、Artistコンポーネントがデータの取得を終え、artistIDプロップに依存してからでないとデータ取得を開始しない
code:app/artist/username/page.tsx
async function Playlists({ artistID }: { artistID: string }) {
// プレイリストのデータを待つ
const playlists = await getArtistPlaylists(artistID)
return (
<ul>
{playlists.map((playlist) => (
<li key={playlist.id}>{playlist.name}</li>
))}
</ul>
)
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// アーティストのデータを待つ
const artist = await getArtist(username)
return (
<>
<h1>{artist.name}</h1>
// q: artistの存在チェックしないの?何かいい感じに待ってくれるんだっけ?
<Suspense fallback={<div>Loading...</div>}>
<Playlists artistID={artist.id} />
</Suspense>
</>
)
}
このような場合、Loading Segment Next.js(Route Segment Next.js用)やReactのSuspence React(ネストされたコンポーネント用)を使って、Reactが結果をStreaming React中に即座にloading states Next.jsを表示できる。これにより、データの取得によってルート全体がブロックされることがなくなり、ユーザーはブロックされていないページの部分と対話できる。
データリクエストのブロッキング:
ウォーターフォールを防ぐ別のアプローチとして、アプリケーションのルートでデータをグローバルに取得する方法がある。
これはデータのロードが完了するまで、それ以下のすべてのルートセグメントのレンダリングをブロック。
「全てか無か」のデータ取得とも言える。
ページまたはアプリケーションの全データが揃っているか、一切ないか、のどちらかともいえる。
今までは、この方法だったんだけど、柔軟にPartial Rendering Next.jsできるようになったということ。
awaitを含む任意のフェッチリクエストは、それがSuspence Reactの境界内でラップされているか、Loading Segment Next.jsが使用されていない限り、それ以下の全ツリーのレンダリングとデータ取得をブロック。別の方法として、Parallel Data Fetchingやpreload patternを使用する方法がある。
hr.icon
Parallel Data Fetching
Parallel Data Fetchingの方法
コンポーネント外でリクエストを定義して、それからコンポーネント内で呼びだす。
メリット
リクエストを並行して開始でき、時間を節約できる。
デメリット/懸念
両方のプロミスが解決されるまで、ユーザーはレンダリングされた結果を見ることができない
感想
これあまり良く分からない。
それぞれ並行にfetchして、それぞれ取得できれば、データ表示すればいいと思うんだけど。
await Promise.all([artistData, albumsData])でまとめる意味がわかんない。
例
getArtistとgetArtistAlbums関数をPageコンポーネントの外で定義し、コンポーネント内で呼び出して、両方のプロミスが解決されるのを待っている。
code: app/artist/username/page.tsx
import Albums from './albums'
// NOTE: API Client側で定義するイメージね。
async function getArtist(username: string) {
const res = await fetch(https://api.example.com/artist/${username})
return res.json()
}
async function getArtistAlbums(username: string) {
const res = await fetch(https://api.example.com/artist/${username}/albums)
return res.json()
}
export default async function Page({
params: { username },
}: {
params: { username: string }
}) {
// 両方のリクエストを並列に開始
const artistData = getArtist(username)
const albumsData = getArtistAlbums(username)
// NOTE: 2つとも同じ箇所で管理するのか。
// プロミスが解決されるのを待つ
const artist, albums = await Promise.all(artistData, albumsData)
return (
<>
<h1>{artist.name}</h1>
<Albums list={albums}></Albums>
</>
)
}
ユーザー体験を向上させるために、Suspence React境界を追加して、レンダリング作業を分割し、可能な限り早く結果の一部を表示できるようにすると良い
q: こういうコト?それとも、2つを1つのSuspence Reactで包む感じ?
(例で書いてくれたらよかったのに。)
code: suspense.tsx
<Suspense fallback={<div>artistName Loading...</div>}>
<h1>{artist.name}</h1>
</Suspense>
<Suspense fallback={<div>Albums Loading...</div>}>
<Albums list={albums}></Albums>
</Suspense>
hr.icon
preload pattern
ウォーターフォールを防ぐ別の方法として、preload patternを使うことができる。
方法
オプションとしてプリロード関数を作成
メリット
Parallel Data Fetchingをさらに最適化することができる。
Promise プロミスをPropsとして渡す必要がなくなる。
プリロード関数はパターンであってAPIではないので、任意の名前をつけることができる
q
何かnext.js側からimportするわけじゃないけど、どうやってpreloadの仕組みは動いているの?
codeだけ見ると、ただの関数に見えるけど。
preload関数で、返り値が無いfetchを行ているのを見るに、事前に叩いてfetch wrapper Next.jsやcache Reactのキャッシュ機構をうまく利用する感じかな?
code: components/Item.tsx
import { getItem } from '@/utils/get-item'
// preload関数を作成。
export const preload = (id: string) => {
// preload関数で、返り値が無いfetchを行う。(事前に叩いてキャッシュ機構をうまく利用する感じかな?)
// voidは与えられた式を評価し、undefinedを返します
void getItem(id)
}
export default async function Item({ id }: { id: string }) {
// Component内でもgetItemでfetchしている。
const result = await getItem(id)
// ...
}
code:app/item/id/page.tsx
import Item, { preload, checkIsAvailable } from '@/components/Item'
export default async function Page({
params: { id },
}: {
params: { id: string }
}) {
// アイテムデータのロードを開始
preload(id)
// 別の非同期タスクを実行
const isAvailable = await checkIsAvailable()
return isAvailable ? <Item id={id} /> : null
}
cache React、server-onlyパッケージ、preload patternを組み合わせることで、アプリ全体で使用できるデータ取得ユーティリティを作成することができる。
q. server-onlyがいるのって、なんで?
a. サーバー上でのデータ取得関数がクライアント上で使用されないようにするために、server-onlyパッケージの使用を推奨する。
code: utils/get-item.ts
import { cache } from 'react'
import 'server-only'
export const preload = (id: string) => {
void getItem(id)
}
export const getItem = cache(async (id: string) => {
// ...
})
このアプローチを使うことで、データを積極的に取得し、レスポンスをキャッシュし、このデータ取得がサーバー上でのみ行われることを保証することができる。
utils/get-itemのエクスポートは、レイアウト、ページ、または他のコンポーネントによって使用され、アイテムのデータがいつ取得されるかを制御することができる。
hr.icon
#### Preventing sensitive data from being exposed to the client
目的
クライアントに機密データが露出するのを防ぐため
方法
taint react(taintObjectReferenceとtaintUniqueValue)を使用する
アプリケーションでtaintingを有効にするには
Next.jsのconfigでexperimental.taintオプションをtrueに設定
その後、taintしたいオブジェクトや値をexperimental_taintObjectReferenceやexperimental_taintUniqueValue関数に渡す。
メリット
オブジェクトのインスタンス全体や機密値がクライアントに渡されるのを防ぐことができる。
感想
意図せず、client側に渡してしまう事増えそうだから、experimentalな機能でも利用したほうがいいんだろうな。
code:app/utils.ts
import { queryDataFromDB } from './api'
import {
experimental_taintObjectReference,
experimental_taintUniqueValue,
} from 'react'
export async function getUserData() {
const data = await queryDataFromDB()
experimental_taintObjectReference(
'クライアントにユーザーオブジェクト全体を渡さないでください',
data
)
experimental_taintUniqueValue(
"クライアントにユーザーの住所を渡さないでください",
data,
data.address
)
return data
}
code: app/page.tsx
import { getUserData } from './data'
export async function Page() {
const userData = getUserData()
return (
<ClientComponent
user={userData} // これはtaintObjectReferenceのためにエラーを引き起こします
address={userData.address} // これはtaintUniqueValueのためにエラーを引き起こします
/>
)
}
セキュリティとサーバーアクションについてもっと学ぶことをおすすめします
How to Think About Security in Next.js | Next.js